package apibasej.db.server.exec;

import java.sql.Connection;
import java.sql.DriverManager;
import java.util.logging.Level;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.sql.DataSource;

import apibasej.basic.misc.UtilTrace;
import apibasej.db.server.ConstantsServerDB;
import apibasej.db.server.pool.PooledConnection;
import apibasej.db.to.StatementQuery;
import apibasej.db.to.StatementValues;
import apibasej.db.transaction.CursorSelect;
import apibasej.db.transaction.GenericTransaction;
import apibasej.db.transaction.UtilDBException;

public class TransactionServer extends GenericTransaction implements ConstantsServerDB{

	// esta classe deve ser usada como uma transação ...
	
	private String url;
	private String user;
	private String pass;
	
	private String dataSourceName;
	
	private DataSource dataSource;
	
	private Connection connection;
	
	private boolean closed = false;
	private Boolean automaticClose = true;

	private TransactionServerInfo infos;
	public TransactionServerInfo getInfos() {
		if(infos==null){
			infos = new TransactionServerInfo();
		}
		return infos;
	}
	
	public TransactionServer() {
		// neste caso o método createConnection() deve ser sobrescrito
		verifyCreate();
	}
	public TransactionServer(String url, String user, String pass) {
		this.url = url;
		this.user = user;
		this.pass = pass;
		verifyCreate();
	}
	public TransactionServer(String dataSourceName) {
		this.dataSourceName = dataSourceName;
		verifyCreate();
	}
	public TransactionServer(DataSource dataSource) {
		this.dataSource = dataSource;
		verifyCreate();
	}
	public TransactionServer(Connection c) {
		this.connection = c;
		verifyCreate();
	}
	
	private void verifyCreate(){
		if(getProp(DEBUG_TRANSACTION_TRACE)) {
			getInfos().setCreatedBy( Thread.currentThread().getStackTrace() );
		}
		if(getProp(DEBUG_TRANSACTION_TIME)){
			getInfos().setNanoTimeCreate(System.nanoTime());
			getInfos().setTimeCreate(getDateNow());
		}
	}
	
	public boolean isAutomaticClose() {
		if(automaticClose==null) automaticClose = true;
		return automaticClose;
	}
	public void setAutomaticClose(boolean automaticClose) { // CUIDADO! se setar false deve ter o controle para sempre fechar depois !!!!
		this.automaticClose = automaticClose;
	}
	
	public Connection getConnection() throws UtilDBException {
		if(closed) {
			String clBy = "";
			if(getProp(DEBUG_TRANSACTION_TRACE)){
				clBy = "[Closed by:\n"+UtilTrace.getStrTrace(getInfos().getClosedBy())+"]";
			}
			throw new UtilDBException("UtilConnection is closed. "+clBy);// não deve deixar ficar criando várias vezes ... depois de fechado já era
		}
		if(connection==null){
			connection = createConnection();
		}
		return connection;
	}
	
	// pode ser sobrescrito
	protected Connection createConnection() throws UtilDBException {
		if(dataSource!=null){
			return getConnByDataSource(dataSource);
		}else if(dataSourceName!=null){
			return getConnByDataSource(dataSourceName);
		}else if(url!=null){
			try {
				return DriverManager.getConnection(url, user, pass);
			} catch (Throwable e) {
				throw new UtilDBException("Error on create connection",e);
			}
		}else{
			throw new UtilDBException("URL or dataSourceName must be defined for default implementation of createConnection()");
		}
	}
	
	public static final Connection getConnByDataSource(DataSource dataSource) throws UtilDBException{
		try {
			Connection connection = dataSource.getConnection();
			if(connection==null)  {
				throw new UtilDBException("Connection not created: "+dataSource);// não deve ocorrer
			}
			return connection;
		} catch (Throwable e) {
			throw new UtilDBException("Error retrieving DataSource connection: "+dataSource, e);
		}
	}
	// para auxiliar quando já esxiste um pool
	public static final Connection getConnByDataSource(String dataSourceJndiName) throws UtilDBException{
		try {
			Context ctx = new InitialContext();
			DataSource ds = (DataSource) ctx.lookup(dataSourceJndiName);
			return getConnByDataSource(ds);
		} catch (Throwable e) {
			throw new UtilDBException("Error retrieving DataSource connection using JNDI: "+dataSourceJndiName, e);
		}
	}
	
	
	// pode ser chamado para controle de transações ou para usar a mesma conexão para várias operações que não precisam de commit (como selects...)
	public void beginTransaction() throws UtilDBException{
		try {
			setAutomaticClose(false);
			
			// deve verificar se já não estava auto-commit, 
			// pois se setar em uma conexão que já tinha transação pode dar erro dependendo do pool
			// se já estiver false não precisa setar de novo
			// EX pool do jboss:  
			//Caused by: java.sql.SQLException: You cannot set autocommit during a managed transaction!
			if(isAutoCommit()){
				getConnection().setAutoCommit(false);
			}
			
			if(getProp(DEBUG_TRANSACTION_TIME)){
				getInfos().setNanoTimeBegin(System.nanoTime());
				getInfos().setTimeBegin(getDateNow());
			}
			
		} catch (Throwable e) {
			verifyConnectionAfterError();// deve ser chamado antes do close()
			close();
			String m = "Error on setAutoCommit(false)";
			throw new UtilDBException(m,e);
		}
	}
	public void commitTransaction(boolean closeConnection) throws UtilDBException{
		try {
			getConnection().commit();
			if(getProp(DEBUG_TRANSACTION_TIME)){//chamar antes do close ... pois deve esta preenchido no beforeClose ...
				getInfos().setNanoTimeCommit(System.nanoTime());
				getInfos().setTimeCommit(getDateNow());
			}
			if(closeConnection){
				close();
			}
		} catch (Throwable e) {
			verifyConnectionAfterError();// deve ser chamado antes do close()
			close();
			String m = "Error on execute commit()";
			throw new UtilDBException(m,e);
		}
	}
	
	// tratar para não lançar execeção..., pois geralmente é chamado em um bloco catch
	public void rollbackTransaction(boolean closeConnection){
		try {
			// ??? verificar se tem problema dar um close em casos de erro antes do rollback()
			if(connection!=null && !closed) {
				connection.rollback();
			}
			if(getProp(DEBUG_TRANSACTION_TIME)){
				getInfos().setNanoTimeRollback(System.nanoTime());
			}
			if(closeConnection){
				close();
			}
		} catch (Throwable e) {
			logDef(Level.SEVERE,"Error on execute rollback()",e);
			close();
		}
	}
	
	public boolean isAutoCommit() throws UtilDBException{
		try {
			return getConnection().getAutoCommit();
		} catch (Exception e) {
			verifyConnectionAfterError();// deve ser chamado antes do close()
			close();
			String m = "Erro on verify auto-commit";
			throw new UtilDBException(m,e);
		}
	}
	
	public boolean isReadOnly() throws UtilDBException{
		try {
			return getConnection().isReadOnly();
		} catch (Exception e) {
			verifyConnectionAfterError();// deve ser chamado antes do close()
			close();
			String m = "Erro on verify isReadOnly()";
			throw new UtilDBException(m,e);
		}
	}
	
	
	@Override
	public void close(){ // boolean isError
		if(connection!=null) { // && !connection.isClosed()
			try{
				if(getProp(DEBUG_TRANSACTION_TIME)){
					getInfos().setNanoTimeClose(System.nanoTime());
					getInfos().setTimeClose(getDateNow());
				}
				
				try {
					TriggerActionsDB trigger = getProp(PROP_TRGGER_ACTIONS_DB);
					// !!! valida para não ter perigo de executar ações sem a trigger, se não quiser fazer nada setar a TriggerActionsDBNull
					if(trigger==null){
						throw new UtilDBException("PROP_TRGGER_ACTIONS_DB not setted!");
					}
					trigger.beforeCloseTransaction(this);
				} catch (Throwable th) {
					logDef(Level.SEVERE, "Error on execute PROP_TRGGER_ACTIONS_DB", th);
				}
				
				//if(!connection.isClosed()){// não verificar, somente tratar para caso esteja fechada, pois não deve estar fechada neste momento
				connection.close();
				connection = null;
				
			}catch (Throwable t) {
				logDef(Level.SEVERE, "Error on close connection in TransactionServer", t);
			}
		}
		closed = true;// independente da conexão, o UtilConnection deve ser invalidado depois de chamar o close()
		if(getProp(DEBUG_TRANSACTION_TRACE)){
			getInfos().setClosedBy( Thread.currentThread().getStackTrace() );//para encontrar bugs ...
		}
	}
	

	
	public boolean isClosed(){
		return closed;
	}
	
	// não contar com esta chamada, pois não é garantido ...
	@Override
	public void finalize() throws Throwable {
		
		// para validar se foi fechado corretamente
		if(connection!=null && !closed){// neste momento o close deve ter sido chamado
			String crBy = "";
			if(getProp(DEBUG_TRANSACTION_TRACE)) {
				crBy = "[Created by:\n"+UtilTrace.getStrTrace(getInfos().getCreatedBy())+"]";
			}
			logDef(Level.SEVERE,"### UtilConnection was not closed! ("+this+") "+crBy+" ###",null);
			close();
		}
		
	}
	
	
	//*********** para o UtilDB *********************
	
	// Verifica se deve fechar a conexao depois das transações no UtilDB
	@Override
	public void verifyCloseAfterExecute(){
		try {
			// verifica !isClosed() para não dar erro no isAutoCommit() se a conexão tiver fechada antes
			//System.out.println("*******>"+isError+"-"+isAutomaticClose()+"-"+isClosed()+"-"+isAutoCommit());
			if(connection!=null && !isClosed() && isAutomaticClose() && isAutoCommit()) {
				close();
			}
		} catch (Throwable t) {
			close();// se der erro fecha
			logDef(Level.SEVERE, "Error on verify connection for close", t);
		}
	}
	@Override
	public int executeUpdate(StatementValues sv) throws Exception {
		return UtilJDBC.executeUpdate(sv,getConnection(),this);
	}
	@Override
	public CursorSelect executeQuery(StatementQuery sv) throws UtilDBException {
		return new CursorSelectImplJdbc(sv, this, getConnection());
	}
	
	

	// usado para verificar a conexão depois que ocorrer algum erro
	// pois o erro pode ter sido por causa da conexão que está inválida
	@Override
	public void verifyConnectionAfterError() {
		if(connection!=null && connection instanceof PooledConnection){
			PooledConnection c = null;
			try{
				c = (PooledConnection)connection;
				c.validadeConnection();
			}catch (Throwable e) {
				if(c!=null){
					c.invalidate();
				}
				logDef(Level.SEVERE,"Error on verify connection after error. InfoConn: "+(c!=null?c.getInfo():"NULL"),e);
			}
		}
	}
}







// Tem que ser chamado no finally. Não tem como fugir.
// Mesmo que esta classe tenha alguns controles não é possível controlar outras exceções que possam ocorrer entre a criação da conexão e o close() automático ou não.
//public void close(){
//	close(false);
//}
/* não verificar isto, pois não tem sentido algum banco fazer commit automático no close
if(isError){
	// tenta rollback
	// segurança para caso alguma implementação de Driver execute commit no close() (de acordo com foruns o Driver da Oracle faz isso)
	// deve ser verificado antes do close
	// não chamar o métodos internos para não ter perigo de entrar em loop infinito
	boolean doRollback = false;
	try{
		//if(!connection.isClosed()){ // não verificar, somente tratar para caso esteja, pois não deve estar fechada neste momento
		
		doRollback = !connection.getAutoCommit() && !connection.isReadOnly();
	}catch (Throwable e2) {
		doRollback = true;
		logSysOut(Level.WARNING,"Error on verify connection.getAutoCommit() or connection.isReadOnly() for close connection",e2);
	}
	if(doRollback){
		try {
			connection.rollback();
		} catch (Throwable e3) {
			logSysOut(Level.WARNING,"Error on execute rollback() for connection with error",e3);
		}
	}
}*/
